<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1"/>
    <title>Custom Cursor (two circles with filter effect and motion delay)</title>

    <style>
        *,
        *::after,
        *::before {
            box-sizing: border-box;
        }

        :root {
            font-size: 15px;
        }

        body {
            margin: 0;
            --color-text: #a3abcd;
            --color-text-alt: #747474;
            --color-bg: #f1f1f1;
            --color-link: #161616;
            --color-link-hover: #797979;
            color: var(--color-text);
            background: var(--color-bg);
            font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif;
            font-weight: 500;
            -webkit-font-smoothing: antialiased;
            -moz-osx-font-smoothing: grayscale;
            --cursor-stroke: #000000;
        }

        a {
            text-decoration: none;
            color: var(--color-link);
            outline: none;
        }

        a:hover {
            color: var(--color-link-hover);
            outline: none;
        }

        /* Better focus styles from https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible */
        a:focus {
            /* Provide a fallback style for browsers
             that don't support :focus-visible */
            outline: none;
            background: lightgrey;
        }

        a:focus:not(:focus-visible) {
            /* Remove the focus indicator on mouse-focus for browsers
             that do support :focus-visible */
            background: transparent;
        }

        a:focus-visible {
            /* Draw a very noticeable focus style for
             keyboard-focus on browsers that do support
             :focus-visible */
            outline: 2px solid red;
            background: transparent;
        }

        main {
            height: 100vh;
            display: flex;
            flex-direction: column;
        }

        .frame {
            padding: 1.5rem 2rem 10vh;
            text-align: center;
            position: relative;
        }

        .frame__title {
            margin: 0;
            font-size: 1rem;
            font-weight: 500;
        }

        .frame__links {
            margin: 0.5rem 0 2rem;
        }

        .frame__links a:not(:last-child) {
            margin-right: 1rem;
        }

        .menu {
            flex: 1;
            display: grid;
            place-items: center;
            align-self: center;
            grid-gap: 7vw;
            font-size: 2rem;
            font-family: "Goudy Old Style", Garamond, "Big Caslon", "Times New Roman", serif;

        }

        .cursor {
            display: none;
        }

        @media screen and (min-width: 53em) {
            .menu {
                font-size: 3vw;
                grid-auto-flow: column;
                max-width: min-content;
            }

            .frame {
                padding: 1.5rem 2rem 0;
                display: grid;
                grid-template-columns: auto 1fr auto;
                grid-template-areas: 'title links sponsor';
                grid-gap: 3vw;
                justify-content: space-between;
                text-align: left;
            }

            .frame__links {
                margin: 0;
            }
        }

        @media (any-pointer: fine) {
            .cursor {
                position: fixed;
                top: 0;
                left: 0;
                display: block;
                pointer-events: none;
                z-index: 10000;
            }

            .cursor__inner {
                fill: none;
                stroke: var(--cursor-stroke);
                stroke-width: 1.2px;
            }

            .no-js .cursor {
                display: none;
            }

        }
    </style>
</head>
<body>
<main>
    <div class="frame">
        <h1 class="frame__title">Sketch 018: Custom Cursor (two circles with filter and delays)</h1>
    </div>
    <nav class="menu">
        <a class="menu__item" href="#">Tathata</a>
        <a class="menu__item" href="#">Upaya</a>
        <a class="menu__item" href="#">Dhyana</a>
        <a class="menu__item" href="#">Manadala</a>
    </nav>
</main>
<svg class="cursor" width="140" height="140" viewBox="0 0 140 140">
    <defs>
        <filter id="cursor-filter" x="-50%" y="-50%" width="200%" height="200%" filterUnits="objectBoundingBox">
            <feTurbulence type="fractalNoise" seed="3" baseFrequency="0" numOctaves="1" result="warp"/>
            <feDisplacementMap xChannelSelector="R" yChannelSelector="G" scale="15" in="SourceGraphic"/>
        </filter>
    </defs>
    <circle class="cursor__inner" cx="70" cy="70" r="20"/>
</svg>
<svg class="cursor" width="140" height="140" viewBox="0 0 140 140" data-amt="0.13">
    <circle class="cursor__inner" cx="70" cy="70" r="24"/>
</svg>

<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.9.1/gsap.min.js"></script>
<script>
    /**
     * Linear interpolation
     * @param {Number} a - first value to interpolate
     * @param {Number} b - second value to interpolate
     * @param {Number} n - amount to interpolate
     */
    const lerp = (a, b, n) => (1 - n) * a + n * b;

    /**
     * Map number x from range [a, b] to [c, d]
     * @param {Number} x - changing value
     * @param {Number} a
     * @param {Number} b
     * @param {Number} c
     * @param {Number} d
     */
    const map = (x, a, b, c, d) => (x - a) * (d - c) / (b - a) + c;

    /**
     * Gets the cursor position
     * @param {Event} ev - mousemove event
     */
    const getCursorPos = ev => {
        return {
            x: ev.clientX,
            y: ev.clientY
        };
    };

    // Track the cursor position
    let cursor = {x: 0, y: 0};
    window.addEventListener('mousemove', ev => cursor = getCursorPos(ev));

    /**
     * Class representing a custom cursor.
     * A Cursor can have multiple elements/svgs
     */
    class Cursor {
        // DOM elements
        DOM = {
            // cursor elements (SVGs .cursor)
            elements: null,
        }
        // All CursorElement instances
        cursorElements = [];

        /**
         * Constructor.
         * @param {NodeList} Dom_elems - all .cursor elements
         * @param {String} triggerSelector - Trigger the cursor enter/leave method on the this selector returned elements. Default is all <a>.
         */
        constructor(Dom_elems, triggerSelector = 'a') {
            this.DOM.elements = Dom_elems;

            [...this.DOM.elements].forEach(el => this.cursorElements.push(new CursorElement(el)));

            [...document.querySelectorAll(triggerSelector)].forEach(link => {
                link.addEventListener('mouseenter', () => this.enter());
                link.addEventListener('mouseleave', () => this.leave());
            });
        }

        /**
         * Mouseenter event
         */
        enter() {
            for (const el of this.cursorElements) {
                el.enter();
            }
        }

        /**
         * Mouseleave event
         */
        leave() {
            for (const el of this.cursorElements) {
                el.leave();
            }
        }
    }

    /**
     * Class representing a .cursor element
     */
    class CursorElement {
        // DOM elements
        DOM = {
            // Main element (.cursor)
            el: null,
            // Inner element (.cursor__inner)
            inner: null,
            // feTurbulence element
            feTurbulence: null
        }
        // Scales value when entering an <a> element
        radiusOnEnter = 50;
        // Opacity value when entering an <a> element
        opacityOnEnter = 1;
        // radius
        radius;
        // Element style properties
        renderedStyles = {
            // With interpolation, we can achieve a smooth animation effect when moving the cursor.
            // The "previous" and "current" values are the values that will interpolate.
            // The returned value will be one between these two (previous and current) at a specific increment.
            // The "amt" is the amount to interpolate.
            // As an example, the following formula calculates the x-axis translation value to apply to the cursor element:
            // this.renderedStyles.tx.previous = lerp(this.renderedStyles.tx.previous, this.renderedStyles.tx.current, this.renderedStyles.tx.amt);

            // translation, scale and opacity values
            // The lower the amt, the slower the cursor "follows" the user gesture
            tx: {previous: 0, current: 0, amt: 0.15},
            ty: {previous: 0, current: 0, amt: 0.15},
            // The scale and opacity will change when hovering over any element specified in [triggerSelector]
            // Defaults are 1 for both properties
            //scale: {previous: 1, current: 1, amt: 0.2},
            radius: {previous: 20, current: 20, amt: 0.15},
            opacity: {previous: 1, current: 1, amt: 0.15}
        };
        // Element size and position
        bounds;
        // SVG filter id
        filterId = '#cursor-filter';
        // for the filter animation
        primitiveValues = {turbulence: 0};

        /**
         * Constructor.
         */
        constructor(DOM_el) {
            this.DOM.el = DOM_el;
            this.DOM.inner = this.DOM.el.querySelector('.cursor__inner');
            this.DOM.feTurbulence = document.querySelector(`${this.filterId} > feTurbulence`);

            this.createFilterTimeline();

            // Hide it initially
            this.DOM.el.style.opacity = 0;

            // Calculate size and position
            this.bounds = this.DOM.el.getBoundingClientRect();

            // Check if any options passed in data attributes
            this.radiusOnEnter = this.DOM.el.dataset.radiusEnter || this.radiusOnEnter;
            this.opacityOnEnter = this.DOM.el.dataset.opacityEnter || this.opacityOnEnter;
            for (const key in this.renderedStyles) {
                this.renderedStyles[key].amt = this.DOM.el.dataset.amt || this.renderedStyles[key].amt;
            }

            this.radius = this.DOM.inner.getAttribute('r');
            this.renderedStyles['radius'].previous = this.renderedStyles['radius'].current = this.radius;

            // Show the element and start tracking its position as soon as the user moves the cursor.
            const onMouseMoveEv = () => {
                // Set up the initial values to be the same
                this.renderedStyles.tx.previous = this.renderedStyles.tx.current = cursor.x - this.bounds.width / 2;
                this.renderedStyles.ty.previous = this.renderedStyles.ty.previous = cursor.y - this.bounds.height / 2;
                // Show it
                this.DOM.el.style.opacity = 1;
                // Start rAF loop
                requestAnimationFrame(() => this.render());
                // Remove the initial mousemove event
                window.removeEventListener('mousemove', onMouseMoveEv);
            };
            window.addEventListener('mousemove', onMouseMoveEv);
        }

        /**
         * Mouseenter event
         * Scale up and fade out.
         */
        enter() {
            //this.renderedStyles['radius'].current = this.radiusOnEnter;
            this.renderedStyles['opacity'].current = this.opacityOnEnter;

            this.filterTimeline.restart();
        }

        /**
         * Mouseleave event
         * Reset scale and opacity.
         */
        leave() {
            this.DOM.inner.style.filter = 'none';
            this.filterTimeline.kill();
            this.renderedStyles['radius'].current = this.radius;
            this.renderedStyles['opacity'].current = 1;
        }

        createFilterTimeline() {
            const turbulenceValues = {from: 0.01, to: 0.04};

            this.filterTimeline = gsap.timeline({
                paused: true,
                onStart: () => {
                    this.DOM.feTurbulence.setAttribute('seed', Math.round(gsap.utils.random(1, 20)));
                    this.DOM.inner.style.filter = `url(${this.filterId}`;
                    this.renderedStyles['opacity'].current = 1;
                },
                onUpdate: () => {
                    this.DOM.feTurbulence.setAttribute('baseFrequency', this.primitiveValues.turbulence);
                    this.renderedStyles['opacity'].current = this.renderedStyles['opacity'].previous = map(this.primitiveValues.turbulence, turbulenceValues.from, turbulenceValues.to, 1, 0);
                    this.renderedStyles['radius'].current = this.renderedStyles['radius'].previous = map(this.primitiveValues.turbulence, turbulenceValues.from, turbulenceValues.to, +this.radius, this.radiusOnEnter);
                },
                onComplete: () => {
                    this.DOM.inner.style.filter = 'none';
                    this.renderedStyles['radius'].current = this.renderedStyles['radius'].previous = this.radius;
                }
            })
                .to(this.primitiveValues, {
                    duration: 1.5,
                    ease: 'power1',
                    startAt: {turbulence: turbulenceValues.from},
                    turbulence: turbulenceValues.to
                });
        }

        /**
         * Loop / Interpolation
         */
        render() {
            // New cursor positions
            this.renderedStyles['tx'].current = cursor.x - this.bounds.width / 2;
            this.renderedStyles['ty'].current = cursor.y - this.bounds.height / 2;

            // Interpolation
            for (const key in this.renderedStyles) {
                this.renderedStyles[key].previous = lerp(this.renderedStyles[key].previous, this.renderedStyles[key].current, this.renderedStyles[key].amt);
            }

            // Apply interpolated values (smooth effect)
            this.DOM.el.style.transform = `translateX(${(this.renderedStyles['tx'].previous)}px) translateY(${this.renderedStyles['ty'].previous}px)`;
            this.DOM.inner.setAttribute('r', this.renderedStyles['radius'].previous);
            this.DOM.el.style.opacity = this.renderedStyles['opacity'].previous;

            // loop...
            requestAnimationFrame(() => this.render());
        }
    }

    // Initialize custom cursor
    const customCursor = new Cursor(document.querySelectorAll('.cursor'));
</script>
</body>
</html>